package abel.wlmr.model;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;

import abel.wlmr.model.type.AfnType;
import abel.wlmr.model.type.EventCountField;
import abel.wlmr.model.type.FrmSeqField;
import abel.wlmr.model.type.TimeBody;
import abel.wlmr.model.type.WlmrAddrBody;
import abel.wlmr.model.type.WlmrControlField;
import abel.wlmr.model.type.WlmrControlField.CtrlAcd;
import abel.wlmr.model.type.WlmrControlField.CtrlDir;
import abel.wlmr.model.unit.Afn00Part;
import abel.wlmr.model.unit.Afn02Part;
import abel.wlmr.model.unit.Afn04DownPart;
import abel.wlmr.model.unit.Afn05DownPart;
import abel.wlmr.model.unit.Afn05UpPart;
import abel.wlmr.model.unit.Afn09UpPart;
import abel.wlmr.model.unit.Afn0FDownPart;
import abel.wlmr.model.unit.Afn10DownPart;
import abel.wlmr.model.unit.AfnFirstPart;
import abel.wlmr.model.unit.AfnQueryPart;
import abel.wlmr.model.unit.AfnSecondDownPart;
import abel.wlmr.model.unit.AfnSecondPart;
import abel.wlmr.model.unit.AfnThirdDownPart;
import abel.wlmr.model.unit.AfnThirdPart;
import io.netty.buffer.ByteBuf;

/**
 * 帧格式 说明 长度
 * （68H）
 * 固定
 * 长度
 * 的报
 * 文头
 * 起始字符（第 1 个） 1 字节
 * 长度 L 长度 2 字节
 * 长度 L 长度（重复） 2 字节
 * （68H） 起始字符（第 2 个） 1 字节
 * 控制域 C 控制域 1 字节
 * 地址域 A 地址域 5 字节
 * 链路用户数
 * 据
 * 应用层
 * （链路用户数据）
 * 用户
 * 数据区
 * （ 长 度 变
 * 化）
 * 校验和 CS 帧校验和 1 字节
 * （16H） 结束字符 1 字节
 * 
 * @author Abel
 *
 */
public class WlmrPacket extends WlmrUserData {
    private final Logger log;

    private static final int MIN_LEN = 6 + 1 + 5 + 1 + 1 + 0 + 1 + 1;

    /**
     * Control / Addr / Afn / Seq.
     */
    private static final short MIN_USER_DATA_LEN = 1 + 5 + 1 + 1;

    private WlmrLengthField length = new WlmrLengthField();;

    private WlmrControlField control = new WlmrControlField();

    private WlmrAddrBody addr = new WlmrAddrBody();

    private AfnType afn;

    private FrmSeqField fseq = new FrmSeqField();

    private WlmrBasePart part1;

    private List<WlmrBasePart> partList = new ArrayList<>();

    private String pwd;

    private EventCountField eventCount = new EventCountField();

    private TimeBody tpv = new TimeBody();

    /**
     * 
     */
    public WlmrPacket(Logger log) {
        super();
        this.log = log;
    }

    /**
     * @param guessedLen
     */
    public WlmrPacket(short guessedLen, Logger log) {
        super(guessedLen);
        this.log = log;
    }

    public ByteBuf appendBuf(ByteBuf buf) {
        // verify fields
        {
            if (control == null) {
                throw new IllegalArgumentException("control can't be null");
            }

            if (addr == null || addr.getA1() == null) {
                throw new IllegalArgumentException("addr can't be null");
            }

            if (afn == null) {
                throw new IllegalArgumentException("afn can't be null");
            }

            if (fseq == null) {
                throw new IllegalArgumentException("fseq can't be null");
            }

            if (partList.size() == 0) {
                throw new IllegalArgumentException("at least one part should exist for " + afn);
            }

            // pwd & events
            if (CtrlDir.Up == control.getDir()) {
                // events
                if (CtrlAcd.HasEvents == control.getAcd() && eventCount == null) {
                    throw new IllegalArgumentException("ec can't be null if has events");
                }
            } else {
                // pwd.
                if (afn.isImportant()) {
                    if (pwd == null || pwd.length() != 16) {
                        throw new IllegalArgumentException("pwd should be 16 bytes long for Down " + afn);
                    }
                }
            }

            if (fseq.isTpv()) {
                if (tpv == null || tpv.getSendTime() == null) {
                    throw new IllegalArgumentException("tpv can't be null if required");
                }
            }
        }

        // set length.
        length.setLength(getFinalLen());

        // begin pad bytes
        buf.writeByte(0x68);
        buf.writeShortLE(length.getValue());
        buf.writeShortLE(length.getValue());
        buf.writeByte(0x68);

        // for check sum.
        int cs0 = buf.writerIndex();

        buf.writeByte(control.getValue());
        addr.appendBuf(buf);

        // user data.
        buf.writeByte(afn.getValue());
        buf.writeByte(fseq.getValue());

        for (WlmrBasePart p : partList) {
            p.appendBuf(buf);
        }

        // {pwd | ec}
        if (CtrlDir.Down == control.getDir()) {
            if (afn.isImportant()) {
                buf.writeBytes(pwd.getBytes(), 0, 16);
            }
        } else {
            if (CtrlAcd.HasEvents == control.getAcd()) {
                buf.writeShortLE(eventCount.getValue());
            }
        }

        // tp
        if (fseq.isTpv()) {
            tpv.appendBuf(buf);
        }

        // checksum.
        buf.writeByte(checkSum(buf.duplicate().readerIndex(cs0)));

        // tail
        buf.writeByte(0x16);

        return buf;
    }

    public int parseBuf(ByteBuf buf) throws WlmrException {
        if (buf.readableBytes() < MIN_LEN)
            return -1;

        // try
        {
            ByteBuf alt = buf.duplicate();

            // 0x68 len len 0x68
            byte start = alt.readByte();
            if (0x68 != start) {
                throw new WlmrLinkException("Start byte should be 0x68 but " + start);
            }
            length = new WlmrLengthField();
            length.parseValue(alt.readShortLE());

            WlmrLengthField l2 = new WlmrLengthField();
            l2.parseValue(alt.readShortLE());
            if (!l2.equals(length)) {
                throw new WlmrLinkException(String.format("length 1 should equal to length 2 but %s != %s",
                    length, l2));
            }
            if (length.getLength() < 1 + 5 + 4) {
                throw new WlmrLinkException("length should be >= 1 + 5 + 4 but " + length.getLength());
            }

            byte end = alt.readByte();
            if (0x68 != end) {
                throw new WlmrLinkException("end byte should be 0x68 but " + end);
            }

            // check length
            if (alt.readableBytes() < length.getLength() + 1 + 1) {
                return -1;
            }

            // verify checksum.
            log.info("CS compare result: {} for packet with length: {}",
                verifyChecksum(alt, length.getLength()), length);
        }

        // real
        {
            buf.skipBytes(6);

            control = new WlmrControlField();
            control.parseValue(buf.readByte());

            log.info("Up control:{}", control);

            addr = new WlmrAddrBody();
            addr.parseBuf(buf);

            // afn
            afn = AfnType.findEnumByValue(buf.readByte());

            log.info("Up {} from {}", afn, addr);

            fseq.parseValue(buf.readByte());

            // check length of user data.
            short ulen = (short) (length.getLength() - 1 - 5 - 1 - 1);
            if (fseq.isTpv()) {
                ulen -= 6;
            }

            // try to parse part list
            int plen;
            if (CtrlDir.Up == control.getDir()) {
                // ec
                if (CtrlAcd.HasEvents == control.getAcd()) {
                    ulen -= 2;
                }

                plen = parseUpBuf(buf, ulen);
            } else {
                if (afn.isImportant() && control.getDir() == CtrlDir.Down) {
                    ulen -= 16;
                }

                plen = parseDownBuf(buf, ulen);
            }

            // check rest bytes.
            if (plen > 0) {
                log.error("FAIL to parse {} bytes 'cause there're some more bytes: {}.", ulen, plen);
                buf.skipBytes(plen);
            }

            // part1
            if (!partList.isEmpty()) {
                part1 = partList.get(0);
            }

            // parse {event counter | pwd } according to direction
            if (CtrlDir.Up == control.getDir()) {
                // event counter
                if (CtrlAcd.HasEvents == control.getAcd()) {
                    eventCount = new EventCountField();
                    eventCount.parseValue(buf.readShortLE());
                }
            } else {
                // pwd only appended for Down
                if (afn.isImportant() && control.getDir() == CtrlDir.Down) {
                    pwd = buf.readCharSequence(16, Charset.defaultCharset()).toString();
                }
            }

            // tp
            if (fseq.isTpv()) {
                tpv.parseBuf(buf);
            }

            // check sum
            buf.skipBytes(1);

            byte tail = buf.readByte();
            if (0x16 != tail) {
                throw new WlmrException("tail should be 0x16 but " + tail);
            }

            // return
            return 1 + 2 + 2 + 1 + length.getLength() + 1 + 1;
        }
    }

    /**
     * This works in station.
     * 
     * @param buf
     * @param ulen
     * @return
     * @throws WlmrException
     */
    private int parseDownBuf(ByteBuf buf, short ulen) throws WlmrException {
        WlmrBasePart part;

        // if there's one id more.
        while (ulen >= 4) {
            switch (afn) {
            case Verify:

            {
                part = new Afn00Part(ulen);
                int plen = part.parseBuf(buf);
                if (plen < 0) {
                    throw new WlmrException("not enough bytes for 00");
                }

                ulen -= plen;
                break;
            }

            case Set:

            {
                part = new Afn04DownPart(ulen);
                int plen = part.parseBuf(buf);
                if (plen < 0) {
                    throw new WlmrException("not enough bytes for 05");
                }

                ulen -= plen;
                break;
            }

            case Ctrl:

            {
                part = new Afn05DownPart(ulen);
                int plen = part.parseBuf(buf);
                if (plen < 0) {
                    throw new WlmrException("not enough bytes for 05");
                }

                ulen -= plen;
                break;
            }

            case Term:

            {
                // only DA/DT in term down.
                part = new WlmrEmptyPart(ulen);
                int plen = part.parseBuf(buf);
                if (plen < 0) {
                    throw new WlmrException("not enough bytes for 09");
                }

                ulen -= plen;
                break;
            }

            case Query:

            {
                // only DA/DT in query down.
                part = new WlmrEmptyPart(ulen);
                int plen = part.parseBuf(buf);
                if (plen < 0) {
                    throw new WlmrException("not enough bytes for 09");
                }

                ulen -= plen;
                break;
            }

            case First:

            {
                // only DA/DT in First down.
                part = new WlmrEmptyPart(ulen);
                int plen = part.parseBuf(buf);
                if (plen < 0) {
                    throw new WlmrException("not enough bytes for 0C");
                }

                ulen -= plen;
                break;
            }

            case Second:

            {
                part = new AfnSecondDownPart(ulen);

                int plen = part.parseBuf(buf);
                if (plen < 0) {
                    throw new WlmrException("not enough bytes for 0D");
                }

                ulen -= plen;
                break;
            }

            case Third:

            {
                part = new AfnThirdDownPart(ulen);
                int plen = part.parseBuf(buf);
                if (plen < 0) {
                    throw new WlmrException("not enough bytes for 0E");
                }

                ulen -= plen;
                break;
            }

            case File:

            {
                part = new Afn0FDownPart(ulen);
                int plen = part.parseBuf(buf);
                if (plen < 0) {
                    throw new WlmrException("not enough bytes for 0E");
                }

                ulen -= plen;
                break;
            }

            case Forward:

            {
                part = new Afn10DownPart(ulen);
                int plen = part.parseBuf(buf);
                if (plen < 0) {
                    throw new WlmrException("not enough bytes for 0E");
                }

                ulen -= plen;
                break;
            }

            default:

            {
                part = new WlmrDefaultPart(ulen);
                int plen = part.parseBuf(buf);
                if (plen < 0) {
                    throw new WlmrException("not enough bytes for 00");
                }

                ulen -= plen;
                break;
            }
            }

            // add to part list
            partList.add(part);
        }

        // return left bytes
        return ulen;

    }

    /**
     * This works in main station.
     * 
     * @param buf
     * @param ulen
     * @return
     * @throws WlmrException
     */
    private short parseUpBuf(ByteBuf buf, short ulen) throws WlmrException {
        WlmrBasePart part = null;

        // if there's one id more.
        try {
            while (ulen >= 4) {
                switch (afn) {
                case Verify:

                {
                    part = new Afn00Part(ulen);
                    int plen = part.parseBuf(buf);
                    if (plen < 0) {
                        throw new WlmrException("not enough bytes for 00");
                    }

                    ulen -= plen;
                    break;
                }

                case Heart:

                {
                    part = new Afn02Part(ulen);
                    int plen = part.parseBuf(buf);
                    if (plen < 0) {
                        throw new WlmrException("not enough bytes for 02");
                    }

                    ulen -= plen;
                    break;
                }

                case Ctrl:

                {
                    part = new Afn05UpPart(ulen);
                    int plen = part.parseBuf(buf);
                    if (plen < 0) {
                        throw new WlmrException("not enough bytes for 05");
                    }

                    ulen -= plen;
                    break;
                }

                case Term:

                {
                    part = new Afn09UpPart(ulen);
                    int plen = part.parseBuf(buf);
                    if (plen < 0) {
                        throw new WlmrException("not enough bytes for 09");
                    }

                    ulen -= plen;
                    break;
                }

                case Query:

                {
                    part = new AfnQueryPart(ulen);
                    int plen = part.parseBuf(buf);
                    if (plen < 0) {
                        throw new WlmrException("not enough bytes for 0A");
                    }

                    ulen -= plen;
                    break;
                }

                case First:

                {
                    part = new AfnFirstPart(ulen);
                    int plen = part.parseBuf(buf);
                    if (plen < 0) {
                        throw new WlmrException("not enough bytes for 0C");
                    }

                    ulen -= plen;
                    break;
                }

                case Second:

                {
                    part = new AfnSecondPart(ulen);

                    int plen = part.parseBuf(buf);
                    if (plen < 0) {
                        throw new WlmrException("not enough bytes for 0D");
                    }

                    ulen -= plen;
                    break;
                }

                case Third:

                {
                    part = new AfnThirdPart(ulen);
                    int plen = part.parseBuf(buf);
                    if (plen < 0) {
                        throw new WlmrException("not enough bytes for 0E");
                    }

                    ulen -= plen;
                    break;
                }

                default:

                {
                    part = new WlmrDefaultPart(ulen);
                    int plen = part.parseBuf(buf);
                    if (plen < 0) {
                        throw new WlmrException("not enough bytes for 00");
                    }

                    ulen -= plen;
                    break;
                }
                }

                // add to part list
                partList.add(part);
            }

            // return left bytes
            return ulen;
        } catch (WlmrException ex) {
            log.error("Fail to parse {} caused by {}", part.getId(), ex.getMessage());
            throw ex;
        }
    }

    /**
     * B.4.3.3.4 帧校验和
     * 帧校验和是用户数据区所有字节的八位位组算术和，不考虑溢出位。用户数据区包括控制域、地址
     * 域、链路用户数据（应用层）三部分。
     * 
     * @param dup
     */
    private byte checkSum(ByteBuf dup) {
        byte[] data = new byte[dup.readableBytes()];
        dup.readBytes(data);

        int cs = 0;
        for (byte b : data) {
            cs += (0xFF & b);
        }

        return (byte) (cs & 0xFF);
    }

    /**
     * B.4.3.3.4 帧校验和
     * 帧校验和是用户数据区所有字节的八位位组算术和，不考虑溢出位。用户数据区包括控制域、地址
     * 域、链路用户数据（应用层）三部分。
     * 
     * @param alt
     * @param len
     * @throws WlmrException
     */
    private boolean verifyChecksum(ByteBuf alt, short len) {
        byte[] data = new byte[len];
        alt.readBytes(data);

        int cs = 0;
        for (byte b : data) {
            cs += (0xFF & b);
        }
        cs &= 0xFF;

        int cs1 = alt.readByte() & 0xFF;
        return (cs == cs1);
    }

    /**
     * @see abel.wlmr.model.WlmrUserData#getFinalLen()
     */
    @Override
    public short getFinalLen() {
        short len = MIN_USER_DATA_LEN;

        // add length of part list.
        for (WlmrBasePart p : partList) {
            len += p.getFinalLen();
        }

        if (CtrlDir.Up == control.getDir()) {
            if (CtrlAcd.HasEvents == control.getAcd()) {
                len += 2;
            }
        } else {
            if (afn.isImportant()) {
                len += 16;
            }
        }

        if (fseq.isTpv()) {
            len += tpv.getFinalLen();
        }

        finalLen = len;

        return finalLen;
    }

    @Override
    public int getMinLength() {
        return MIN_LEN;
    }

    /* (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return String.format(
            "{length:%s, control:%s, addr:%s, afn:%s, fseq:%s, part1:%s, partList:%s, pwd:%s, eventCount:%s, tpv:%s}",
            length, control, addr, afn, fseq, part1, partList, pwd, eventCount, tpv);
    }

    /**
     * @return the length
     */
    public WlmrLengthField getLength() {
        return length;
    }

    /**
     * @param length
     *            the length to set
     */
    public void setLength(WlmrLengthField length) {
        if (length == null) {
            throw new IllegalArgumentException("length can't be null");
        }

        this.length = length;
    }

    /**
     * @return the control
     */
    public WlmrControlField getControl() {
        return control;
    }

    /**
     * @param control
     *            the control to set
     */
    public void setControl(WlmrControlField control) {
        if (control == null) {
            throw new IllegalArgumentException("control can't be null");
        }

        this.control = control;
    }

    /**
     * @return the addr
     */
    public WlmrAddrBody getAddr() {
        return addr;
    }

    /**
     * @param addr
     *            the addr to set
     */
    public void setAddr(WlmrAddrBody addr) {
        if (addr == null) {
            throw new IllegalArgumentException("addr can't be null");
        }

        this.addr = addr;
    }

    /**
     * @return the afn
     */
    public AfnType getAfn() {
        return afn;
    }

    /**
     * @param afn
     *            the afn to set
     */
    public void setAfn(AfnType afn) {
        if (afn == null) {
            throw new IllegalArgumentException("afn can't be null");
        }

        this.afn = afn;
    }

    /**
     * @return the fseq
     */
    public FrmSeqField getFseq() {
        return fseq;
    }

    /**
     * @param fseq
     *            the fseq to set
     */
    public void setFseq(FrmSeqField fseq) {
        if (fseq == null) {
            throw new IllegalArgumentException("fseq can't be null");
        }

        this.fseq = fseq;
    }

    /**
     * @return the part1
     */
    public WlmrBasePart getPart1() {
        if (part1 == null && partList.size() > 0) {
            part1 = partList.get(0);
        }

        return part1;
    }

    /**
     * @param part1
     *            the part1 to set
     */
    public void setPart1(WlmrBasePart part1) {
        if (part1 == null) {
            throw new IllegalArgumentException("part1 can't be null");
        }

        this.part1 = part1;

        partList.add(0, part1);
    }

    /**
     * @return the partList
     */
    public List<WlmrBasePart> getPartList() {
        return partList;
    }

    /**
     * @param partList
     *            the partList to set
     */
    public void setPartList(List<WlmrBasePart> partList) {
        if (partList == null) {
            throw new IllegalArgumentException("partList can't be null");
        }

        this.partList = partList;
        part1 = partList.size() > 0 ? partList.get(0) : null;
    }

    /**
     * @return the pwd
     */
    public String getPwd() {
        return pwd;
    }

    /**
     * @param pwd
     *            the pwd to set
     */
    public void setPwd(String pwd) {
        if (pwd == null || pwd.length() != 16) {
            throw new IllegalArgumentException("pwd should be 16 long if required");
        }

        this.pwd = pwd;
    }

    /**
     * @return the ec
     */
    public EventCountField getEventCount() {
        return eventCount;
    }

    /**
     * @param ec
     *            the ec to set
     */
    public void setEventCount(EventCountField ec) {
        if (ec == null) {
            throw new IllegalArgumentException("event count can't be null");
        }

        this.eventCount = ec;
    }

    /**
     * @return the tpv
     */
    public TimeBody getTpv() {
        return tpv;
    }

    /**
     * @param tpv
     *            the tpv to set
     */
    public void setTpv(TimeBody tpv) {
        if (tpv == null) {
            throw new IllegalArgumentException("tpv can't be null");
        }

        this.tpv = tpv;
    }

    /**
     * May skip some bytes if not head - 0x68 XX YY XX YY 0x68.
     * 
     * @param in
     * @deprecated
     */
    public void tryFindHead(ByteBuf in) {
        if (in.readableBytes() < 6) {
            return;
        }

        int len = 0;
        ByteBuf dup = in.duplicate();
        while (dup.readableBytes() > 6) {
            ++len;

            if (dup.readByte() != 0x68) {
                continue;
            }

            if (dup.readableBytes() > 5) {
                dup.markReaderIndex();
                if (dup.readShortLE() == dup.readShortLE() && dup.readByte() == 0x68) {
                    break;
                } else {
                    dup.resetReaderIndex();
                }
            } else {
                len = 0;
                break;
            }
        }

        // check if need to skip.
        if (len > 0) {
            log.warn("SKIP {} bytes", len - 1);
            in.skipBytes(len - 1);
        }
    }

}
